package ace.module.oauth2.server.core.impl.authentication.ace;

import ace.cmp.json.api.JsonService;
import ace.module.oauth2.server.api.model.input.Oauth2AceAuthenticationInput;
import ace.module.oauth2.server.api.model.input.params.Oauth2AceAuthParamsMap;
import ace.module.oauth2.server.core.impl.authentication.ace.service.AceAuthenticationService;
import ace.module.oauth2.server.core.impl.util.Oauth2AssertUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

/**
 * @author caspar
 * @date 2023/2/8 13:40 为spring authorization server 添加 oauth2 的 rest 模式
 */
@AllArgsConstructor
@Component
public class Oauth2AceAuthenticationConverter implements AuthenticationConverter {

  private final AceAuthenticationService aceAuthenticationService;
  private final JsonService jsonService;
  private final TypeReference<Oauth2AceAuthenticationInput<Oauth2AceAuthParamsMap>> defaultRequestTypeReference = new TypeReference<>() {
  };

  @Override
  public OAuth2AuthorizationGrantAuthenticationToken convert(HttpServletRequest request) {
    // grant_type (REQUIRED)
    String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
    if (!aceAuthenticationService.getAuthorizationGrantType().getValue().equals(grantType)) {
      return null;
    }

    OAuth2ClientAuthenticationToken clientPrincipal = (OAuth2ClientAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    String bodyString = this.getBodyString(request);
    Oauth2AceAuthenticationInput<Oauth2AceAuthParamsMap> defaultBodyModel = this.convertDefaultRequest(bodyString);

    MultiValueMap<String, String> parameters = this.getParameters(request);

    // scope (OPTIONAL)
    String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
    if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
      Oauth2AssertUtils.throwError(
          OAuth2ErrorCodes.INVALID_REQUEST,
          OAuth2ParameterNames.SCOPE,
          Oauth2AssertUtils.ERROR_URI);
    }

    Set<String> requestedScopes = new HashSet<>();
    if (StringUtils.hasText(scope)) {
      requestedScopes.addAll(StringUtils.commaDelimitedListToSet(scope));
    }

    Map<String, Object> additionalParameters = new HashMap<>();
    parameters.forEach(
        (key, value) -> {
          if (!key.equals(OAuth2ParameterNames.GRANT_TYPE)) {
            additionalParameters.put(key, value.get(0));
          }
        });

    Oauth2AceAuthenticationToken oauth2AceAuthenticationToken =
        new Oauth2AceAuthenticationToken(
            aceAuthenticationService.getAuthorizationGrantType(),
            clientPrincipal,
            bodyString,
            defaultBodyModel,
            requestedScopes,
            additionalParameters);
    return oauth2AceAuthenticationToken;
  }

  protected Oauth2AceAuthenticationInput<Oauth2AceAuthParamsMap> convertDefaultRequest(String bodyString) {
    return this.jsonService.toObject(bodyString, this.defaultRequestTypeReference.getType());
  }

  protected MultiValueMap<String, String> getParameters(HttpServletRequest request) {
    Map<String, String[]> parameterMap = request.getParameterMap();
    MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
    parameterMap.forEach(
        (key, values) -> {
          for (String value : values) {
            parameters.add(key, value);
          }
        }
    );
    return parameters;
  }

  @SneakyThrows
  protected String getBodyString(HttpServletRequest request) {
    try (InputStream is = request.getInputStream()) {
      return IOUtils.toString(is, StandardCharsets.UTF_8);
    }
  }
}
